03 ImageLoader 三级缓存

01 基础框架
02 请求队列
03 三级缓存
04 图片加载
05 常见问题
06 项目源码

前面博客中使用了内存缓存,我们先来回答一个问题:为什么要使用缓存?

移动设备在使用网络时往往面临一个问题,那就是流量是需要收费的,这就需要软件开发者在开发时应当尽量避免流量的消耗,而流量消耗的大头就是图片。这时候本地缓存就是一个很好的解决方式。而且移动设备用户所处的网络环境也是不可知的,如果用户处于弱网络环境下,那图片加载所要消耗的时间将是不可忍受的,这也是使用本地缓存的原因之一。

同时,移动设备的内存是有限的,如果一个应用包含大量的图片,全部放到内存中必然会触发 OOM,可如果每次都要重新从本次磁盘加载的话,性能就会有很大的消耗。而且本地加载虽然比网络要快但也是需要时间的,这也往往造成界面的卡顿。这时候一个好的内存缓存策略就是不可或缺的。

明白了为什么使用,接下来就要考虑怎么实现了。大家先看一张图:

首先,程序会在内存缓存中查找 Bitmap,如果命中则直接显示,如果没有就会去本地磁盘缓存中查找缓存文件;如果在磁盘缓存中命中就将缓存文件转换为 Bitmap 再进行显示,这个过程中会将 Bitmap 加入内存缓存中;如果本地磁盘中没有就会从网络上进行下载,并且缓存在磁盘和内存中。

现在我们先来实现磁盘缓存(DiskCache):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class DiskCache {
// 图片缓存 SD 卡目录
private static String CACHE_DIR =
Environment.getExternalStorageDirectory() + "/";
// 从 SD 卡缓存中获取图片
public Bitmap get(BitmapRequest request) {
return BitmapFactory.decodeFile(CACHE_DIR + urlToMd5(request.imageUri));
}
// 将图片缓存到 SD 卡中
public void put(BitmapRequest request, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(CACHE_DIR + urlToMd5(request.imageUri));
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private String urlToMd5(String url) {
if (TextUtils.isEmpty(url)) {
return "";
}
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(url.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(Integer.toHexString(b & 0xff));
}
hex.append(".png");
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("NoSuchAlgorithmException", e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException", e);
}
}
}

然后修改 ImageLoader.java 源码进行测试,可使用:

1
2
3
4
5
6
public class ImageLoader {
// 图片缓存
// private ImageCache mImageCache = new ImageCache();
private DiskCache mImageCache = new DiskCache();
...
}

接下来要实现的是:首先使用内存缓存,如果内存缓存没有图片再使用 SD 卡缓存,如果 SD 卡中也没有图片,最后才从网络上获取。于是新建一个双缓存类 DoubleCache.java,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DoubleCache {
private ImageCache mMemoryCache = new ImageCache();
private DiskCache mDiskCache = new DiskCache();
public Bitmap get(BitmapRequest request) {
Bitmap bitmap = mMemoryCache.get(request);
if (bitmap == null) {
bitmap = mDiskCache.get(request);
}
return bitmap;
}
public void put(BitmapRequest request, Bitmap bitmap) {
mMemoryCache.put(request, bitmap);
mDiskCache.put(request, bitmap);
}
}

很快就发现,ImageCache、DiskCache 和 DoubleCache,有共同的行为,那就是对 Bitmap 的存取操作,只是具体的实现细节不同。此处,我们可以对他们进行抽象出共同的接口 IBitmapCache,有 get、put 和 remove 方法。对于数据源,一般都是提供增删改查操作。此时 ImageCache 类名改为 MemoryCache 更恰当。

IBitmapCache.java

1
2
3
4
5
public interface IBitmapCache {
void put(BitmapRequest key, Bitmap value);
Bitmap get(BitmapRequest key);
void remove(BitmapRequest key);
}

MemoryCache.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MemoryCache implements IBitmapCache {
private static final int ONE_KB = 1024;
private static final int MAX_MEMORY_PERCENT = 4;
// 图片内存缓存
private LruCache<BitmapRequest, Bitmap> mImageCache;
public MemoryCache() {
initImageCache();
}
// 初始化缓存大小:取四分之一的可用内存作为缓存。
private void initImageCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / ONE_KB);
int cacheSize = maxMemory / MAX_MEMORY_PERCENT;
mImageCache = new LruCache<BitmapRequest, Bitmap>(cacheSize) {
@Override
protected int sizeOf(BitmapRequest key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / ONE_KB;
}
};
}
public void put(BitmapRequest key, Bitmap value) {
mImageCache.put(key, value);
}
public Bitmap get(BitmapRequest key) {
return mImageCache.get(key);
}
public void remove(BitmapRequest key) {
mImageCache.remove(key);
}
}

DoubleCache.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DoubleCache implements IBitmapCache {
private MemoryCache mMemoryCache = new MemoryCache();
private DiskCache mDiskCache = new DiskCache();
@Override
public Bitmap get(BitmapRequest key) {
Bitmap bitmap = mMemoryCache.get(key);
if (bitmap == null) {
bitmap = mDiskCache.get(key);
}
return bitmap;
}
@Override
public void put(BitmapRequest key, Bitmap bitmap) {
mMemoryCache.put(key, bitmap);
mDiskCache.put(key, bitmap);
}
@Override
public void remove(BitmapRequest key) {
mMemoryCache.remove(key);
mDiskCache.remove(key);
}
}

DiskCache.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class DiskCache implements IBitmapCache {
// 图片缓存 SD 卡目录
private static String CACHE_DIR =
Environment.getExternalStorageDirectory() + "/";
// 从 SD 卡缓存中获取图片
public Bitmap get(BitmapRequest request) {
return BitmapFactory.decodeFile(CACHE_DIR + urlToMd5(request.imageUri));
}
// 将图片缓存到 SD 卡中
public void put(BitmapRequest request, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(CACHE_DIR + urlToMd5(request.imageUri));
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
public void remove(BitmapRequest request) {
if (request != null && !TextUtils.isEmpty(request.imageUri)) {
File file = new File(CACHE_DIR + urlToMd5(request.imageUri));
if (file.exists()) {
file.delete();
}
}
}
private String urlToMd5(String url) {
if (TextUtils.isEmpty(url)) {
return "";
}
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(url.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(Integer.toHexString(b & 0xff));
}
hex.append(".png");
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("NoSuchAlgorithmException", e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException", e);
}
}
}

接下来重构 ImageLoader 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ImageLoader {
// 图片内存缓存
private IBitmapCache mCache = new MemoryCache();
// 代码省略...
public void init(ImageLoaderConfig config) {
mConfig = config;
mCache = mConfig.imageCache;
checkConfig();
mImageQueue = new RequestQueue(mConfig.threadCount);
mImageQueue.start();
}
private void checkConfig() {
if (mCache == null) {
mCache = new MemoryCache();
}
// 代码省略...
}
public IBitmapCache getCache() {
return mCache;
}
// 代码省略...
}

ImageLoaderConfig 类修改同上,略。

经过此次重构,用户可以通过 setCache(IBitmapCache cache) 函数设置缓存实现,也就是通常说的依赖注入。具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IBitmapCache cache1 = new MemoryCache();
IBitmapCache cache2 = new DiskCache();
IBitmapCache cache3 = new DoubleCache();
IBitmapCache cache4 = new IBitmapCache() {
@Override
public Bitmap get(BitmapRequest key) {
return null; // 从缓存中获取图片
}
@Override
public void put(BitmapRequest key, Bitmap bitmap) {
// 缓存图片
}
@Override
public void remove(BitmapRequest key) {
// 删除
}
};

在上述代码中,通过 setCache(IBitmapCache cache) 方法注入不同的缓存实现,这样不仅能够使 ImageLoader 更简单、健壮,也使得 ImageLoader 的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache 缓存图片的具体实现完全不一样,但是,它们的一个特点是,都实现了 IBitmapCache 接口。当用户需要自定义实现缓存策略时,只需要新建一个实现 IBitmapCache 接口的类,然后构造该类的对象,并且通过 setCache 函数注入到 ImageLoaderConfig 中,这样 ImageLoader 就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致 ImageLoader 类的修改。

接下来,我们再新增一个缓存类 NoCache:

1
2
3
4
5
6
7
8
9
10
11
12
public class NoCache implements IBitmapCache {
@Override
public Bitmap get(BitmapRequest key) {
return null;
}
@Override
public void put(BitmapRequest key, Bitmap value) {
}
@Override
public void remove(BitmapRequest key) {
}
}

然后就没有然后了,因为不需要修改其他模块的任何代码,这就是抽象的魅力,而且几乎符合六大原则:功能单一,满足单一职责原则;对扩展开放对修改关闭,满足开闭原则;setBitmapCache(IBitmapCache cache) 的用法同时满足里氏替换原则依赖倒置原则;IBitmapCache 接口类几乎不可拆分,满足接口隔离原则;对于 ImageLoader 来说最直接的朋友是 IBitmapCache,至于你依赖 LruCache 对象还是 File 文件系统,都与我无关,符合迪米特原则

在上面代码中,内存缓存我们使用了官方的 LruCache 类,对于磁盘缓存,我们选择 Jake Wharton 大神的 DiskLruCache 类。先来看看 DiskCache 改造后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class DiskCache implements IBitmapCache {
// 1 MB
private static final int MB = 1024 * 1024;
// cache dir
private static final String IMAGE_DISK_CACHE = "bitmap";
// Disk LRU Cache
private DiskLruCache mDiskLruCache;
// Disk Cache Instance
private static DiskCache mDiskCache;
private String mCachePath;
private DiskCache(Context context) {
initDiskCache(context);
}
private void initDiskCache(Context context) {
try {
File cacheDir = getDiskCacheDir(context, IMAGE_DISK_CACHE);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache
.open(cacheDir, getAppVersion(context), 1, 50 * MB);
} catch (IOException e) {
e.printStackTrace();
}
}
// 单例
public static DiskCache getDiskCache(Context context) {
if (mDiskCache == null) {
synchronized (DiskCache.class) {
if (mDiskCache == null) {
mDiskCache = new DiskCache(context);
}
}
}
return mDiskCache;
}
// 从 SD 卡缓存中获取图片
@Override
public Bitmap get(BitmapRequest request) {
final InputStream inputStream = getInputStream(Md5Helper.toMD5(request.imageUri));
return BitmapFactory.decodeStream(inputStream);
}
// 将图片缓存到磁盘中
@Override
public void put(BitmapRequest request, Bitmap value) {
DiskLruCache.Editor editor;
try {
// 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
editor = mDiskLruCache.edit(Md5Helper.toMD5(request.imageUri));
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (writeBitmapToDisk(value, outputStream)) {
// 写入disk缓存
editor.commit();
} else {
editor.abort();
}
IOUtil.closeQuietly(outputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void remove(BitmapRequest key) {
try {
mDiskLruCache.remove(Md5Helper.toMD5(key.imageUri));
} catch (IOException e) {
e.printStackTrace();
}
}
private InputStream getInputStream(String md5) {
Snapshot snapshot;
try {
snapshot = mDiskLruCache.get(md5);
if (snapshot != null) {
return snapshot.getInputStream(0);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private File getDiskCacheDir(Context context, String name) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
Log.d("", "### context : " + context + ", dir = " + context.getExternalCacheDir());
mCachePath = context.getExternalCacheDir().getPath();
} else {
mCachePath = context.getCacheDir().getPath();
}
return new File(mCachePath + File.separator + name);
}
private boolean writeBitmapToDisk(Bitmap bitmap, OutputStream outputStream) {
BufferedOutputStream bos = new BufferedOutputStream(outputStream, 8 * 1024);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
boolean result = true;
try {
bos.flush();
} catch (IOException e) {
e.printStackTrace();
result = false;
} finally {
IOUtil.closeQuietly(bos);
}
return result;
}
private int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(),
0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
}

其中有些方法已经放到其他工具类中,如 Md5Helper、IOUtil;有些没有,如 getDiskCacheDir()、getAppVersion() 等,这些细节暂不表。此处用了单例模式,没有持有 Context 对象,防止内存泄漏。如果必须持有 Context 对象,建议使用下面方式:

1
mContext = context.getApplicationContext();

因为创建 DiskCache 需要 Context 对象,对它依赖的对象都要修改代码。在本项目中只有 DoubleCache 中持有磁盘缓存对象,所以修改代码不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DoubleCache implements IBitmapCache {
private MemoryCache mMemoryCache = new MemoryCache();
private DiskCache mDiskCache;
public DoubleCache(Context context) {
mDiskCache = DiskCache.getDiskCache(context);
}
@Override
public Bitmap get(BitmapRequest key) {
Bitmap value = mMemoryCache.get(key);
if (value == null) {
value = mDiskCache.get(key);
// 存到内存缓存中
saveBitmapIntoMemory(key, value);
}
return value;
}
@Override
public void put(BitmapRequest key, Bitmap bitmap) {
mMemoryCache.put(key, bitmap);
mDiskCache.put(key, bitmap);
}
@Override
public void remove(BitmapRequest key) {
mMemoryCache.remove(key);
mDiskCache.remove(key);
}
private void saveBitmapIntoMemory(BitmapRequest key, Bitmap bitmap) {
// 如果 Value 从 disk 中读取,那么存入内存缓存
if (bitmap != null) {
mMemoryCache.put(key, bitmap);
}
}
}

另附工具类代码 IOUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class IOUtil {
private IOUtil() {}
/**
* 关闭IO
* @param closeables closeables
*/
public static void close(Closeable... closeables) {
if (closeables == null) return;
for (Closeable closeable : closeables) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 关闭IO,不输出日志
* @param closeables closeables
*/
public static void closeQuietly(Closeable... closeables) {
if (closeables == null) return;
for (Closeable closeable : closeables) {
if (closeable != null) {
try { closeable.close(); } catch (IOException e) {}
}
}
}
}

Md5Helper.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Md5Helper {
/**
* 使用MD5算法对传入的key进行加密并返回。
*/
private static MessageDigest mDigest = null;
static{
try {
mDigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
/**
*
* 对key进行MD5加密,如果无MD5加密算法,则直接使用key对应的hash值。</br>
* @param key
* @return
*/
public static String toMD5(String key) {
String cacheKey;
//获取MD5算法失败时,直接使用key对应的hash值
if ( mDigest == null ) {
return String.valueOf(key.hashCode());
}
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
return cacheKey;
}
/**
* @param bytes
* @return
*/
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}